baubs git
Commit 8e84cbf609f301edcbf4affdf3723fb77e7b5847
Parents : a253901
Author : Jan-Henrik Bruhn <hi@jhbruhn.de>
Date : 2025-12-19T12:43:54+01:00
fix: Implement Content Security Policy and secure COOP/COEP headers
- Add strict Content Security Policy to protect against XSS attacks
- Implement custom app:// protocol for production builds with proper headers
- Enable secure cross-origin isolation for SharedArrayBuffer support
- Remove insecure --enable-features bypass flag
- Add proper COOP/COEP/CORP headers for all resources
- Allow Pyodide workers to function in production builds
This fixes critical security vulnerabilities while maintaining full
functionality including Pyodide web workers and SharedArrayBuffer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes
Diff
diff --git a/electron/main.ts b/electron/main.ts
index 14b8c40..cb76852 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -1,4 +1,4 @@
-import { app, BrowserWindow, ipcMain, dialog } from "electron";
+import { app, BrowserWindow, ipcMain, dialog, protocol } from "electron";
import { join } from "path";
import { promises as fs } from "fs";
import Store from "electron-store";
@@ -22,8 +22,73 @@ updateElectronApp({
app.commandLine.appendSwitch("enable-web-bluetooth", "true");
app.commandLine.appendSwitch("enable-experimental-web-platform-features");
+// Register custom protocol for serving app files with proper COOP/COEP headers
+// This is required for SharedArrayBuffer support in production builds
+protocol.registerSchemesAsPrivileged([
+ {
+ scheme: "app",
+ privileges: {
+ standard: true,
+ secure: true,
+ supportFetchAPI: true,
+ corsEnabled: false,
+ bypassCSP: false,
+ },
+ },
+]);
+
const store = new Store();
+// Setup custom protocol handler for production builds
+async function setupCustomProtocol() {
+ protocol.handle("app", async (request) => {
+ // Parse the URL to get the file path
+ const url = new URL(request.url);
+ let filePath = decodeURIComponent(url.pathname);
+
+ // Handle Windows paths (remove leading slash)
+ if (process.platform === "win32" && filePath.startsWith("/")) {
+ filePath = filePath.slice(1);
+ }
+
+ // Resolve relative to app resources
+ const resourcePath = join(__dirname, "..", "renderer", MAIN_WINDOW_VITE_NAME, filePath);
+
+ try {
+ const data = await fs.readFile(resourcePath);
+
+ // Determine content type from extension
+ const ext = filePath.split(".").pop()?.toLowerCase();
+ const contentTypes: Record<string, string> = {
+ html: "text/html",
+ js: "application/javascript",
+ css: "text/css",
+ json: "application/json",
+ png: "image/png",
+ jpg: "image/jpeg",
+ jpeg: "image/jpeg",
+ svg: "image/svg+xml",
+ wasm: "application/wasm",
+ whl: "application/zip",
+ };
+ const contentType = contentTypes[ext || ""] || "application/octet-stream";
+
+ // Return response with proper COOP/COEP/CORP headers
+ return new Response(data, {
+ headers: {
+ "Content-Type": contentType,
+ "Cross-Origin-Opener-Policy": "same-origin",
+ "Cross-Origin-Embedder-Policy": "require-corp",
+ "Cross-Origin-Resource-Policy": "same-origin",
+ },
+ });
+ } catch (err) {
+ console.error("[CustomProtocol] Failed to load:", resourcePath, err);
+ return new Response("Not Found", { status: 404 });
+ }
+ });
+}
+
function createWindow() {
const mainWindow = new BrowserWindow({
width: 1600,
@@ -38,8 +103,7 @@ function createWindow() {
contextIsolation: true,
sandbox: true,
webSecurity: true,
- // Enable SharedArrayBuffer for Pyodide
- additionalArguments: ["--enable-features=SharedArrayBuffer"],
+ // SharedArrayBuffer enabled via proper COOP/COEP headers below
},
});
@@ -106,21 +170,16 @@ function createWindow() {
// Set COOP/COEP headers for Pyodide SharedArrayBuffer support
mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => {
- // Apply headers to ALL resources including workers
+ // Apply security headers to enable cross-origin isolation (needed for SharedArrayBuffer)
const headers: Record<string, string[]> = {
...details.responseHeaders,
- "Cross-Origin-Opener-Policy": ["unsafe-none"],
- "Cross-Origin-Embedder-Policy": ["unsafe-none"],
+ "Cross-Origin-Opener-Policy": ["same-origin"],
+ "Cross-Origin-Embedder-Policy": ["require-corp"],
+ // Add CORP to ALL resources since this is a local-only app
+ // This allows workers, Pyodide assets, and all other resources to load
+ "Cross-Origin-Resource-Policy": ["same-origin"],
};
- // For same-origin resources (including workers), add CORP header
- if (
- details.url.startsWith("http://localhost") ||
- details.url.startsWith("file://")
- ) {
- headers["Cross-Origin-Resource-Policy"] = ["same-origin"];
- }
-
callback({ responseHeaders: headers });
},
);
@@ -131,14 +190,18 @@ function createWindow() {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
mainWindow.webContents.openDevTools();
} else {
- // MAIN_WINDOW_VITE_NAME is the renderer name from forge.config.js
- mainWindow.loadFile(
- join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
- );
+ // Production: Use custom protocol to serve files with proper COOP/COEP/CORP headers
+ // This enables SharedArrayBuffer support for Pyodide
+ mainWindow.loadURL("app://./index.html");
}
}
-app.whenReady().then(createWindow);
+app.whenReady().then(async () => {
+ // Setup custom protocol for production builds
+ await setupCustomProtocol();
+
+ createWindow();
+});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
diff --git a/index.html b/index.html
index e014908..351af00 100644
--- a/index.html
+++ b/index.html
@@ -2,6 +2,23 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
+ <!-- Content Security Policy - Strict local-only configuration -->
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="
+ default-src 'self' app:;
+ script-src 'self' app: 'wasm-unsafe-eval';
+ style-src 'self' app: 'unsafe-inline';
+ img-src 'self' app: data: blob:;
+ font-src 'self' app: data:;
+ connect-src 'self' app:;
+ worker-src 'self' app: blob:;
+ child-src 'none';
+ object-src 'none';
+ base-uri 'self';
+ form-action 'none';
+ "
+ />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Respira</title>
Served by rngit 1.3.3 - Generated in 0.04s